const GU = require('../gameUtils.js');
const {ACTION_CODE, STATE, GAME_MODE, CARD, CARD_GROUP} = require('./chudadiConstants.js');
const {canFollow, calculateCardScore} = require('./checkFollow.js');
const ChudadiPlayer = require('./chudadiPlayer.js');
const Game = require('../base/game.js');
const util = require('util');
const StateMachine = require('javascript-state-machine');
const config = require('../../config.json');
const Log = require('../logger.js')({logPath: config.logPath});
const Cheat = require('./cheatData.js');

var ChudadiGame = function (data, config) {
    Game.call(this);
    let self = this;
    // 将数据合并到本对象
    for (let key in data) this[key] = data[key];
    // 初始化玩家对象
    this.players = {};
    this.users.forEach(user => self.players[user.id] = new ChudadiPlayer(user));
    // 设置配置对象
    let defaultConfig = {
        needPlayerCount: 4,
        cards: [10,11,12,13,20,21,22,23,30,31,32,33,40,41,42,43,50,51,52,53,60,61,62,63,70,71,72,73,80,81,82,83,90,91,92,93,100,101,102,103,110,111,112,113,120,121,122,123,130,131,132,133],
        handCardCount: 13,
        cheat: 0,
        mode: GAME_MODE.DEVELOP
    };
    this.config = Object.assign(defaultConfig, config || {});
    // 重置游戏变量
    // TODO: 不应该在构造的时候初始化，应该再start里初始化
    this.othersActionList = []; // 当前玩家打牌后，其他玩家的可执行动作列表
    // TODO: 是不是真的每个玩家都要有自己的压缩工具呢？
    this.jsonCompressors = {};
    this.users.forEach(u => self.jsonCompressors[u.id] = new GU.JsonCompressor);
    // this.jsonCompressor = new GU.JsonCompressor; // json压缩工具
    // 重写游戏状态
    // 使用 javascript-state-machine 做状态机
    this.fsm = new StateMachine({
        init: STATE.NONE,
        transitions: [ // 各transition
            {name: 'start', from: STATE.NONE, to: STATE.WAIT_PLAYER_ACTION},
            {name: 'playCard', from: STATE.WAIT_PLAYER_ACTION, to: STATE.WAIT_OHTERS_ACTION},
            {name: 'followCard', from: STATE.WAIT_OHTERS_ACTION, to: STATE.WAIT_OHTERS_ACTION},
            {name: 'pass', from: STATE.WAIT_OHTERS_ACTION, to: STATE.WAIT_OHTERS_ACTION},
            {name: 'next', from: STATE.WAIT_OHTERS_ACTION, to: STATE.WAIT_PLAYER_ACTION},
            {name: 'gameOver', from: [STATE.WAIT_PLAYER_ACTION,STATE.WAIT_OHTERS_ACTION], to: STATE.GAME_OVER}
        ],
        methods: {
            onBeforeStart () {
                console.log('onStart');
                // 通知更新玩家状态（整个状态）
                self._syncGameState();
            },
            onBeforePlayCard () {
                console.log('onBeforePlayCard');
                const currentPlayer = self.players[self.currentPlayerId]
                console.log('currentPlayerId', self.currentPlayerId)
                self._clearActionData(self.currentPlayerId)
                // 检查是否游戏结束
                if (currentPlayer.handCards.length === 0) {
                    // TODO 在一个transition中，不能进行另外一个transition，这里做个延时
                    setTimeout(function () {
                        self.fsm.gameOver(self.currentPlayerId)
                    }, 1000)
                    return
                }
                // 获取其他玩家可执行动作队列
                self.othersActionList = self._retrieveOthersActionList();
                // 先将当前玩家打牌的状态更新到客户端
                self._syncGameState();
            },
            onBeforeFollowCard () {
                console.log('onBeforeFollowCard');
                // 获取其他玩家可执行动作队列
                const currentPlayer = self.players[self.currentPlayerId]
                console.log('currentPlayerId', self.currentPlayerId)
                self._clearActionData(self.currentPlayerId);
                // 检查是否游戏结束
                if (currentPlayer.handCards.length === 0) {
                    // TODO 在一个transition中，不能进行另外一个transition，这里做个延时
                    setTimeout(function () {
                        self.fsm.gameOver(self.currentPlayerId)
                    }, 1000)
                    return
                }
                self.othersActionList = self._retrieveOthersActionList();
                console.log('othersActionList', JSON.stringify(self.othersActionList))
                // TODO 跟牌不会转换状态，这里只能手动触发一次
                self._checkNextOthersAction(false);
                // 先将当前玩家打牌的状态更新到客户端
                self._syncGameState();
            },
            onBeforePass (transition, playerId) {
                console.log('onPass');
                self._clearActionData(playerId);
                self._syncGameState();
                // TODO: 与暗杠/碰后杠类似的问题，由于fsm的原因，下面的代码实际是onWaitOthersAction，考虑优化
                setTimeout(function () {
                    self._checkNextOthersAction(false);
                    self._syncGameState();
                }, 1000);
            },
            onBeforeNext () {
                console.log('onBeforeNext');
                let lastPlayer = self.players[self.currentPlayerId];
                lastPlayer.playCardEnd();
                // self.currentPlayerId = self._getNextPlayerId();
                // let player = self.players[self.currentPlayerId];
                // player.drawCard(self._getTopCard());
            },
            onWaitPlayerAction () {
                console.log('onWaitPlayerAction');
                self._clearActionData();
                self.othersActionList = [];
                let player = self._updateCurrentPlayerAction();
                // TODO: 通知更新玩家状态（整个状态）
                self._syncGameState();
                console.log(`通知玩家${self.currentPlayerId}可执行动作: `, player.actionCode);
            },
            onWaitOthersAction () {
                console.log('onWaitOthersAction');
                // 等1s，解决fsm的上一个transition还未完成就next的问题
                // TODO: 这个解决办法不是很好，考虑别的解决办法
                setTimeout(function () {
                    self._checkNextOthersAction();
                }, 1000);
            },
            onGameOver (playerId) {
                console.log('onGameOver', playerId + ' win!');
                self._syncGameState();
                // TODO: 这里应存储更多的房间信息
                Log.record(self.roomId, JSON.stringify({stateHistory: self.stateHistory}));
                // self._sendToPlayer(JSON.stringify({msg:"游戏结束"}));
            }
        }
    });
};

var p = ChudadiGame.prototype;

// 实现Game的接口
// 开始游戏
p.canStart = function () {
    return true;
};
p.start = async function () {
    let self = this;
    // TODO: 重置单局数据
    self.stateHistory = [];
    self.othersActionList = [];
    // self.currentPlayerId = self.playerSequence[0];
    self._resetCards();
    self._resetPlayerData();
    // 发牌
    self._dealCards();
    // TODO: 通知发牌结果
    // 整理手牌
    for (let playerId in self.players) self.players[playerId].sortHandCard();
    // TODO: 通知整理手牌结果

    // 锄大地中，手中有方块3的玩家先出牌
    self.currentPlayerId = Object.keys(self.players).find(playerId => self.players[playerId].handCards.includes(CARD.DIAMOND_3))
    // 回合制游戏属性初始化
    // TODO 这个东西还没实际使用
    this.turnCount = 0;
    
    // TODO: 进入等待当前玩家动作状态
    self.fsm.start();
    // self.state = self.STATE.WAIT_CURRENT_PLAYER_ACTION;
    return {'error': false, result: '游戏开始'};
};
// 断线重连
// TODO: 考虑以后加入玩家的是否离线状态，所以这里应该是对所有玩家进行广播
p.reconnect = function (playerId, socket) {
    console.log(`玩家${playerId}重连`);
    this.players[playerId].socket = socket;
    this._sendToPlayer(JSON.stringify(this._getGameState(playerId, false)), playerId, true);
};

// 玩家动作接口
// 玩家动作总接口
// 这里通过状态机统一验证player能否做action，然后再分别调用子action动作进行详细验证
p.doAction = function (playerId, action, data) {
    let message = '';
    // 缺少动作合法性判断（动作是否存在）
    // TODO: 这里的错误信息应该更详细
    if (this.fsm.is(STATE.WAIT_PLAYER_ACTION) && playerId !== this.currentPlayerId) {
        message = '在等待当前玩家动作时，非当前玩家请求动作';
    }
    if (this.fsm.is(STATE.WAIT_OHTERS_ACTION) && playerId === this.currentPlayerId) {
        if (playerId === this.currentPlayerId)
            message = '在等待其他玩家动作时，当前玩家请求动作';
        // to check: 此时this.othersActionList能确保有至少1个元素么？
        else if (playerId !== this.othersActionList[0].playerId)
            message = '在等待其他玩家动作时，非当前询问玩家请求动作';
    }
    if (this.fsm.cannot(action)) {
        message = '当前游戏状态下，玩家不可以执行该动作';
    }
    if (message) {
        return {'error': true, 'result': message};
    } else {
        // to check: 是否所有动作都能正确调用，及返回
        let res = this[action](playerId, data);
        if (res.error) {
            return res;
        } else {
            this.fsm[action](playerId);
            return res;
        }
    }
};
// 出牌
p.playCard = function (playerId, cardIndexes) {
    let message = '', player = this.players[playerId];
    // 先检查出的牌是否存在
    if (!player.hasCardIndexes(cardIndexes)) {
        message = `玩家${playerId}手中没有这些${cardIndexes.map(cardIndex => cardIndex + 1).join(',')}张牌`;
    }
    // 如果有方块3，一定要打出来
    let diamond_3_idx = player.handCards.indexOf(CARD.DIAMOND_3)
    if (diamond_3_idx >= 0 && !cardIndexes.includes(diamond_3_idx)) {
        message = `玩家${playerId}手中有方块3可是没有打出`;
    }
    // 校验出的牌是否符合某一个牌型
    let cards = cardIndexes.map(cardIndex => player.handCards[cardIndex])
    const cardsInfo = calculateCardScore(cards)
    if (cardsInfo.type === CARD_GROUP.NONE) {
        message = `打出的牌${cards}，不符合任何牌型`
    }
    if (message) { // 如果有message，意味着有错误
        return {'error': true, 'result': message};
    } else {
        player.playCard(cardIndexes);
        // player.sortHandCard();
        message = `玩家${playerId}打出${player.playingCards}`;
        return {'error': false, 'result': message};
    }
};
// 跟牌
p.followCard = function (playerId, cardIndexes) {
    console.log('followCard', playerId, cardIndexes)
    let currentPlayer = this.players[this.currentPlayerId],
        player = this.players[playerId],
        playingCards = currentPlayer.playingCards;
    // 先检查出的牌是否存在
    if (!player.hasCardIndexes(cardIndexes)) {
        return {error: true, result: `玩家${playerId}手中没有这些${cardIndexes.map(cardIndex => cardIndex + 1).join(',')}张牌`}
    }
    // 映射出跟牌数组
    const followCards = cardIndexes.map(cardIndex => player.handCards[cardIndex])
    // 校验是否能跟牌
    const res = canFollow(playingCards, followCards)
    // TODO 成功后做的事情本来应该放在fsm的onFollowCardBefore做的，但是那边没有传入playerId，暂时先在这里做
    if (!res.error) {
        this._clearActionData(this.currentPlayerId)
        // 跟牌成功，之前打牌的玩家收回playingCards
        currentPlayer.playCardEnd()
        // 跟牌成功要切换当前玩家
        this.currentPlayerId = playerId
        // 当前玩家跟牌作为出牌
        player.playCard(cardIndexes)
    }
    return res
};

/**
 * 过
 * @param {String} playerId - 玩家id
 */
p.pass = function (playerId) {
    // 除了合法性检测不需要干别的事
    let player = this.players[playerId];
    if (player.actionCode&ACTION_CODE.Pass) {
        return {'error': false, 'result': `玩家${playerId}跳过，他当前可执行动作为${player.actionCode}`};
    } else {
        return {'error': true, 'result': `玩家${playerId}不可执行过动作`};
    }
};

// 私有接口
// 用于管理麻将牌
/**
 * 返回顶部的卡牌
 * @param {Number} card - 希望获取的卡牌，如果不传，则正常返回顶部卡牌
 * @return {Number}
 */
p._getTopCard = function (card) {
    if (this.cardCount <= 0) return -1;
    if (card != undefined) {
        let idx = this.cards.indexOf(card);
        if (idx != -1 && this.topIdx<=idx && idx<=this.bottomIdx) {
            let temp = this.cards[idx];
            this.cards[idx] = this.cards[this.topIdx];
            this.cards[topIdx] = temp;
        }
    } else {
        card = this.cards[this.topIdx];
    }
    this.topIdx++;
    this.cardCount--;
    return card;
};

/**
 * 返回顶部的卡牌
 * @param {Number} card - 希望获取的卡牌，如果不传，则正常返回顶部卡牌
 * @return {Number}
 */
p._getBottomCard = function (card) {
    if (this.cardCount <= 0) return -1;
    if (card != undefined) {
        let idx = this.cards.indexOf(card);
        if (idx != -1 && this.topIdx<=idx && idx<=this.bottomIdx) {
            let temp = this.cards[idx];
            this.cards[idx] = this.cards[this.bottomIdx];
            this.cards[bottomIdx] = temp;
        }
    } else {
        card = this.cards[this.bottomIdx];
    }
    this.bottomIdx--;
    this.cardCount--;
    return card;
};

// 获取剩余可抽牌数
p._getCardRemain = function () {
    return this.cardCount;
};

// 初始化麻将牌相关数据
// 根据config.cheat决定是否使用作弊数据
p._resetCards = function () {
    this.cards = GU.shuffle([].concat(this.config.cards));
    this.config.cheat && (this.cards = Cheat.getCheatCards(this.config, this.config.cheat));
    this.topIdx = 0;
    this.bottomIdx = this.cards.length - 1;
    this.cardCount = this.cards.length;
};
/**
 * 清空玩家的actionCode
 * @param {String|Array.<String>|undefined} playerId - 需要情况的玩家id，若不传则清除所有玩家的可执行动作
 */
p._clearActionData = function (playerId) {
    let self = this, playerIds = playerId ? [].concat(playerId) : self.playerSequence;
    playerIds.forEach(playerId => self.players[playerId].clearActionData());
};

// 初始化方法
/**
 * 重置单局玩家数据
 */
p._resetPlayerData = function () {
    let self = this;
    this.playerSequence.forEach(playerId => self.players[playerId]._resetData());
};

/**
 * 发牌（按playerSequence顺序来轮流抽handCardCount张牌）
 */
p._dealCards = function () {
    let self = this;
    for (let i=0; i<this.config.handCardCount; i++) {
        this.playerSequence.forEach(playerId => self.players[playerId].handCards.push(self._getTopCard()));
    }
};

// 检查当前玩家能做什么动作，锄大地就只有打牌这个动作
p._updateCurrentPlayerAction = function () {
    let playerId = this.currentPlayerId,
        player = this.players[playerId],
        actionCode = ACTION_CODE.Play;
    player.setActionCode(actionCode);
    return player;
};
/**
 * 在当前玩家打出牌后，计算出其他玩家的可执行动作列表，以优先度排序
 */
// TODO: 以下方法名的retrieve我都觉得不合适，以后考虑改名
// 对于锄大地来说，就很简单，其他玩家能做的事情就是跟牌或者过
p._retrieveOthersActionList = function () {
    let currentPlayer = this.players[this.currentPlayerId],
        // currentPlayCard = currentPlayer.playingCards,
        otherPlayerIds = this._getOtherPlayerIdsByOrder(),
        res = [];
    for (let playerId of otherPlayerIds) { // 分别统计出玩家的可执行动作码
        // TODO 服务端需要计算出玩家是否能跟牌吗？
        // 这里先简单认为每个玩家都可以跟牌、过
        res.push({playerId, actionCode: ACTION_CODE.Follow | ACTION_CODE.Pass})
    }
    return res;
};

// 检查下一个其他玩家的可执行动作
// TODO: 感觉这个方法命名不好，耦合度很高，考虑优化
p._checkNextOthersAction = function (notify = true) {
    let playerAction = this.othersActionList.shift();
    if (playerAction === undefined) { // 没有其他玩家可执行动作，进入下一回合
        this.fsm.next();
    } else {
        let player = this.players[playerAction.playerId];
        // 通知玩家更新界面状态（动作按钮状态更新）
        player.setActionCode(playerAction.actionCode);
        if (notify) {
            this._sendToPlayer(JSON.stringify(this._getGameState(player.id)), player.id);
            console.log(`通知玩家${player.id}: `, playerAction);
        }
    }
};

// 工具方法
// 获取下一个玩家的id
p._getNextPlayerId = function (playerId) {
    playerId = playerId || this.currentPlayerId;
    if (!this.playerSequence.includes(playerId)) console.error(`playerId ${playerId} 在this.playerSequence ${this.playerSequence} 中找不到 in chudadiGame._getNextPlayerId`);
    return this.playerSequence[(this.playerSequence.indexOf(playerId)+1)%this.playerSequence.length];
};
// 获取除 playerId 之外的玩家id数组（按顺序的）
p._getOtherPlayerIdsByOrder = function (playerId) {
    playerId = playerId || this.currentPlayerId;
    let ps = this.playerSequence,
        idx = ps.indexOf(playerId);
    return ps.slice(idx+1, ps.length).concat(ps.slice(0, idx));
};

/** 获取游戏状态
 * @param {String|undefined} playerId - 玩家id，传入时，对除该玩家外的手牌、暗杠等数据遮罩。不传则全部返回
 * @param {Boolean} [incremental=true] - 是否增量，为真时，只返回新增信息，为假时返回全量信息
*/
p._getGameState = function (playerId, incremental = true) {
    let self = this;
    // 桌面数据应该是对所有玩家公开的
    let tableData = {
        cardRemain: this._getCardRemain(),
        currentPlayerId: this.currentPlayerId,
        state: this.fsm.state
    };
    // 有传入playerId时只传回本人数据，其他玩家数据屏蔽
    let playerDatas = {};
    // 只公开自己的信息
    this.playerSequence.forEach(pid => playerDatas[pid] = self.players[pid].getData(!!playerId && pid != playerId, incremental));
    return {tableData, playerDatas, incremental};
};

// 通知某玩家信息
p._sendToPlayer = function (msg, playerId, fullUncompressMap = false) {
    if (!playerId || !this.players[playerId]) {
        console.log(`error: 不存在玩家 ${playerId} in ChudadiGame._sendToPlayer`);
        return false;
    }
    Log.log(`send to ${playerId}`, msg);
    // TODO: 这里为了方便，将msg转回JSON格式，实际应该将调用_sendToPlayer的地方的JSON.stringify都去掉
    // 压缩数据
    let msgJson = JSON.parse(msg);
    let jsonCompressor = this.jsonCompressors[playerId];
    msgJson = jsonCompressor.compress(msgJson); // 压缩json
    msgJson.uncompressMap = jsonCompressor.exportUncompressMap(fullUncompressMap); // 导出新的解压表
    let msgCompressedStr = jsonCompressor.toCompressString(msgJson); // 转成压缩字符串
    // 将信息传回前端
    this.players[playerId].socket.emit('news', msgCompressedStr);
};

// 同步游戏状态
p._syncGameState = function (playerIds) {
    let self = this;
    playerIds = playerIds || self.playerSequence;
    playerIds.forEach(playerId => self._sendToPlayer(JSON.stringify(self._getGameState(playerId)), playerId));
    // 取完整状态作为日志记录
    self.stateHistory.push(self._getGameState())
    // 读完数据再清理
    playerIds.forEach(playerId => self.players[playerId].dirty.clear());
};

util.inherits(ChudadiGame, Game);

module.exports = ChudadiGame;